package com.hospital.register.core.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.hospital.register.common.exception.BusinessException;
import com.hospital.register.common.result.ResultCodeEnum;
import com.hospital.register.common.utils.DateUtil;
import com.hospital.register.core.repository.ScheduleRepository;
import com.hospital.register.core.service.DepartmentService;
import com.hospital.register.core.service.HospitalService;
import com.hospital.register.core.service.ScheduleService;
import com.hospital.register.dto.core.api.ScheduleRemoveDto;
import com.hospital.register.dto.core.common.ScheduleDaysDto;
import com.hospital.register.dto.core.common.SchedulePageDto;
import com.hospital.register.model.core.BookingRule;
import com.hospital.register.model.core.Department;
import com.hospital.register.model.core.Schedule;
import com.hospital.register.vo.core.api.ScheduleDetailVo;
import com.hospital.register.vo.core.common.HospitalQueryVo;
import com.hospital.register.vo.core.common.ScheduleDayVo;
import com.hospital.register.vo.core.common.ScheduleDaysVo;
import com.hospital.register.vo.core.common.ScheduleQueryVo;
import com.hospital.register.vo.order.OrderMessageVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

@Service
public class ScheduleServiceImpl implements ScheduleService {

    /**
     * 访问排班数据
     */
    @Autowired
    private ScheduleRepository scheduleRepository;

    /**
     * 访问 Mongo
     */
    @Autowired
    private MongoTemplate mongoTemplate;

    /**
     * 处理医院相关业务
     */
    @Autowired
    private HospitalService hospitalService;

    /**
     * 处理科室相关业务
     */
    @Autowired
    private DepartmentService departmentService;

    /**
     * 年月日日期格式
     */
    private final String datePattern = "yyyy-MM-dd";

    /**
     * 日期格式
     * 年、月、日、十、分
     */
    private final String dateMinutePattern = "yyyy-MM-dd HH:mm";

    /**
     * 存放 DateTimeFormatter 实例
     */
    ThreadLocal<DateFormat> dateFormatBox = new ThreadLocal<>();

    @Override
    public boolean existed(String hoscode, String hosScheduleId) {
        return this.scheduleRepository.existed(hoscode, hosScheduleId);
    }

    @Override
    public boolean insert(Schedule schedule) {
        return this.scheduleRepository.insert(schedule);
    }

    @Override
    public boolean update(Schedule schedule) {
        return this.scheduleRepository.update(schedule);
    }

    @Override
    public Page<ScheduleQueryVo> findPage(SchedulePageDto conditions) {
        Page<ScheduleQueryVo> page = this.scheduleRepository.findPage(conditions);
        if (page.getContent() == null || page.getContent().isEmpty()) {
            return page;
        }

        Calendar calendar = Calendar.getInstance();
        for (ScheduleQueryVo schedule : page.getContent()) {
            this.packageSchedule(schedule, calendar);
        }
        return page;
    }

    @Override
    public boolean remove(ScheduleRemoveDto dto) {
        return this.scheduleRepository.remove(dto);
    }

    @Override
    public boolean existInFuture(String hoscode, String depcode) {
        return this.scheduleRepository.existInFuture(hoscode, depcode);
    }

    @Override
    public ScheduleDaysVo findDataOfDays(ScheduleDaysDto dto) {
        return this.scheduleRepository.findDataOfDays(dto);
    }

    @Override
    public ScheduleDaysVo findFutureData(ScheduleDaysDto conditions) {
        //查医院
        HospitalQueryVo hospital = this.hospitalService.findByHoscode(conditions.getHoscode());
        if (hospital == null) {
            throw new BusinessException("医院不存在", ResultCodeEnum.DATA_ERROR.getCode());
        }

        BookingRule bookingRule = hospital.getBookingRule();
        if (bookingRule == null) {
            throw new BusinessException("医院的预约规则不存在", ResultCodeEnum.DATA_ERROR.getCode());
        }

        //根据预约周期生成预查询的日期列表（分页）
        IPage<Date> datePage = this.guessScheduledDays(conditions.getPage(), conditions.getLimit(), bookingRule);
        if (datePage == null || datePage.getRecords() == null || datePage.getRecords().isEmpty()) {//日期列表为空，手动构造的，基本不可能
            throw new BusinessException("预查询的排班日期推断失败", ResultCodeEnum.DATA_ERROR.getCode());
        }

        //查找排班数据
        conditions.setWorkDates(datePage.getRecords());
        conditions.setPage(1);//参数里的分页属性是给其它业务用的

        ScheduleDaysVo result = this.scheduleRepository.findDataOfDays(conditions);
        if (result == null) {//基本不可能，因为这是手动构造的
            throw new BusinessException("分页统计近期的排班数据失败", ResultCodeEnum.FAIL.getCode());
        }

        //查出来的是不可变集合，需要转成可变集合
        List<ScheduleDayVo> resultList = new ArrayList<>(conditions.getWorkDates().size());
        if (result.getBookingScheduleList() != null && result.getBookingScheduleList().size() > 0) {
            resultList.addAll(result.getBookingScheduleList());
        }

        result.setBookingScheduleList(resultList);
        result.setBookingScheduleRuleList(resultList);//两个接口参数名不一样

        //便利要查询的每一天，如果当天没有排班，手动构造一个无号的排班，方便页面展示
        Comparator<ScheduleDayVo> comparator = ScheduleDayVo.getWorkDateComparator();
        Collections.sort(resultList, comparator);//需要可变集合

        ScheduleDayVo key = new ScheduleDayVo();
        Calendar calendar = Calendar.getInstance();
        for (Date workDate : conditions.getWorkDates()) {
            if (workDate == null) {
                continue;
            }

            key.setWorkDate(workDate);
            int index = Collections.binarySearch(resultList, key, comparator);
            if (index < 0) {//当天没有排班医生
                index = Math.abs(index + 1);//正确插入位置

                calendar.setTimeInMillis(workDate.getTime());//定位到那一天
                int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);

                ScheduleDayVo target = this.constructNonExistent(workDate, dayOfWeek);
                resultList.add(index, target);
            }
        }

        //今天如果过了停号时间， 不能预约，修改第一条记录
        if (Long.valueOf(1L).equals(datePage.getCurrent())) {
            Date now = new Date();
            //获取今天的停号时间（分钟）
            Date todayStopTime = this.generateMinute(now, bookingRule.getStopTime());
            if (now.getTime() > todayStopTime.getTime()) {
                resultList.get(0).setStatus(-1);
            }
        }

        //最后一页最后一条记录为即将预约
        if (Long.valueOf(datePage.getCurrent()).equals(datePage.getPages())) {
            resultList.get(resultList.size() - 1).setStatus(1);
        }

        return result;
    }

    @Override
    public ScheduleQueryVo findById(String id) {
        ScheduleQueryVo schedule = this.scheduleRepository.findById(id);
        this.packageSchedule(schedule, Calendar.getInstance());
        return schedule;
    }

    @Override
    public ScheduleDetailVo findDetailById(String id) {
        ScheduleQueryVo schedule = this.scheduleRepository.findById(id);
        if (schedule == null) {
            throw new BusinessException("对应排班不存在", ResultCodeEnum.PARAM_ERROR.getCode());
        }

        Department department = this.departmentService.findOne(schedule.getHoscode(), schedule.getDepcode());
        if (department == null) {
            throw new BusinessException("排班所在的科室查询失败", ResultCodeEnum.PARAM_ERROR.getCode());
        }

        HospitalQueryVo hospital = this.hospitalService.findByHoscode(schedule.getHoscode());
        if (hospital == null) {
            throw new BusinessException("排班所在的医院查询失败", ResultCodeEnum.PARAM_ERROR.getCode());
        }

        BookingRule bookingRule = hospital.getBookingRule();
        if (bookingRule == null) {
            throw new BusinessException("排班所在医院的预约规则查询失败", ResultCodeEnum.PARAM_ERROR.getCode());
        }

        //计算退号时间（就诊日期减去退号天数）
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(schedule.getWorkDate());
        calendar.add(Calendar.DAY_OF_MONTH, bookingRule.getQuitDay());//减去一定的天数
        Date quitTime = this.generateMinute(calendar.getTime(), bookingRule.getQuitTime());
        calendar.clear();

        //计算今天开始预约的时间
        Date now = new Date();
        Date startTime = this.generateMinute(now, bookingRule.getReleaseTime());

        //今天停止挂号时间
        Date todayStopTime = this.generateMinute(now, bookingRule.getStopTime());

        //计算预约截止时间
        calendar.setTime(now);
        calendar.add(Calendar.DAY_OF_MONTH, bookingRule.getCycle());
        Date endTime = this.generateMinute(calendar.getTime(), bookingRule.getStopTime());

        //重新封装结果，因为有一些属性的值数据库中查不出来，需要计算，还有一些属性的名称和数据库中定义的不一样。
        ScheduleDetailVo result = new ScheduleDetailVo();
        result.setId(schedule.getId());
        result.setHoscode(schedule.getHoscode());
        result.setHosname(hospital.getHosname());
        result.setDepcode(schedule.getDepcode());
        result.setDepname(department.getDepname());
        result.setHosScheduleId(schedule.getHosScheduleId());
        result.setTitle(schedule.getTitle());
        result.setReserveDate(schedule.getWorkDate());
        result.setAvailableNumber(schedule.getAvailableNumber());
        result.setReserveTime(schedule.getWorkTime());
        result.setAmount(schedule.getAmount());
        result.setQuitTime(quitTime);
        result.setStartTime(startTime);
        result.setEndTime(endTime);
        result.setStopTime(todayStopTime);
        return result;
    }

    @Override
    public boolean updateNumberById(OrderMessageVo order) {
        return this.scheduleRepository.updateNumberById(order);
    }

    /**
     * 构造一个无号的排班日期
     *
     * @param workDate  具体的日期
     * @param dayOfWeek 那天的星期
     * @return 一个无号的排班日期
     */
    private ScheduleDayVo constructNonExistent(Date workDate, int dayOfWeek) {
        ScheduleDayVo schedule = new ScheduleDayVo();
        schedule.setDocCount(0);
        schedule.setAvailableNumber(0);
        schedule.setReservedNumber(0);
        schedule.setWorkDate(workDate);
        schedule.setWorkDateMd(workDate);
        schedule.setDayOfWeek(DateUtil.getChineseDayOfWeek(dayOfWeek));
        schedule.setStatus(0);
        return schedule;
    }

    /**
     * 获得指定日期的某一分钟
     *
     * @param thatDay       指定日期
     * @param hourAndMinute 小时:分钟
     *                      格式： HH:mm
     * @return 日期+时间
     */
    private Date generateMinute(Date thatDay, String hourAndMinute) {
        DateFormat dateFormat = this.dateFormatBox.get();
        if (dateFormat == null) {
            dateFormat = new SimpleDateFormat(this.dateMinutePattern);
            this.dateFormatBox.set(dateFormat);
        }
        //将当天的年月日，与具体时间拼接
        String datePattern = DateUtil.formatDate(thatDay);
        String minutePattern = datePattern + " " + hourAndMinute;
        try {
            return dateFormat.parse(minutePattern);
        } catch (ParseException e) {
            throw new BusinessException("数据库中存储的时间存在格式错误：" + hourAndMinute, ResultCodeEnum.DATA_ERROR.getCode());
        }
    }

    /**
     * 分页产生未来几天的日期列表，今天为第0天
     *
     * @param page        页码（从1开始）
     * @param limit       每页行数
     * @param bookingRule 医院的预约规则
     * @return 日期列表
     */
    private IPage<Date> guessScheduledDays(int page, int limit, BookingRule bookingRule) {
        Date now = new Date();
        //获取医院的预约周期
        int totalDays = bookingRule.getCycle();

        if (page < 1) {
            page = 1;
        }

        if (limit < 1 || limit > totalDays) {
            limit = totalDays;
        }

        int start = page * limit - limit;//起始点从0开始，页码从1开始
        int end = page * limit - 1;

        if (start > totalDays) {
            start = totalDays - limit;//最后一夜
        }

        if (start < 0) {
            start = 0;
        }

        //生成今天的放号时间（分钟）
        Date todayReleaseTime = this.generateMinute(now, bookingRule.getReleaseTime());

        //如果已经过了今天的放号时间，预约周期从明天开始计算，周期+1
        if (now.getTime() > todayReleaseTime.getTime()) {
            totalDays = totalDays + 1;
        }

        //推断未来N天的排班日期（不精确到小时和分钟），最后一天显示即将放号
        List<Date> onePage = new ArrayList<>(limit);

        //首先找到今天的0.0分0秒，其它日期都在此基础上累加
        long startOfToday = DateUtil.getStartOnOneDay(todayReleaseTime).getTime();

        if (start == 0) {
            onePage.add(new Date(startOfToday));
            start++;
        }

        for (int i = 0; i < totalDays; i++) {
            if (i >= start && i <= end) {//在页码内
                Date row = new Date(startOfToday + i * DateUtil.MILLISECONDS_PER_DAY);
                onePage.add(row);
            }
        }

        IPage<Date> pageResult = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, limit, totalDays);
        pageResult.setRecords(onePage);
        return pageResult;
    }

    /**
     * 填充排班的其它信息
     *
     * @param schedule 排班信息
     * @param calendar 日期对象（调用层的方法内部可共享）
     */
    private void packageSchedule(ScheduleQueryVo schedule, Calendar calendar) {
        calendar.setTimeInMillis(schedule.getWorkDate().getTime());//定位到那一天
        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        String hoscode = schedule.getHoscode();
        String depcode = schedule.getDepcode();

        HospitalQueryVo hospital = this.hospitalService.findByHoscode(hoscode);
        Department department = this.departmentService.findOne(hoscode, depcode);

        ScheduleQueryVo.OtherInfo otherInfo = new ScheduleQueryVo.OtherInfo();
        otherInfo.setHosname(hospital.getHosname());//医院名称
        otherInfo.setDepname(department.getDepname());//科室名称
        otherInfo.setDayOfWeek(DateUtil.getChineseDayOfWeek(dayOfWeek));//星期
        schedule.setParam(otherInfo);
    }

}
